# Neo4j Knowledge Graph Complete Tutorial
## From Fundamentals to Production Graph RAG

**Version 1.0 | January 2025**

---

# Table of Contents

1. [Introduction to Graph Databases](#1-introduction-to-graph-databases)
2. [Environment Setup](#2-environment-setup)
3. [Nodes, Labels, and Properties](#3-nodes-labels-and-properties)
4. [Relationships](#4-relationships)
5. [Basic CRUD Operations](#5-basic-crud-operations)
6. [Advanced Cypher Queries](#6-advanced-cypher-queries)
7. [Indexing and Constraints](#7-indexing-and-constraints)
8. [Transaction Management](#8-transaction-management)
9. [Batch Operations](#9-batch-operations)
10. [Schema Design Patterns](#10-schema-design-patterns)
11. [Graph Data Science Basics](#11-graph-data-science-basics)
12. [Centrality Algorithms](#12-centrality-algorithms)
13. [Community Detection](#13-community-detection)
14. [Pathfinding Algorithms](#14-pathfinding-algorithms)
15. [Similarity Algorithms](#15-similarity-algorithms)
16. [Link Prediction](#16-link-prediction)
17. [Graph Embeddings](#17-graph-embeddings)
18. [Ontology Design](#18-ontology-design)
19. [Populating Ontologies](#19-populating-ontologies)
20. [Document Chunking](#20-document-chunking)
21. [Vector Embeddings](#21-vector-embeddings)
22. [HNSW Indexing](#22-hnsw-indexing)
23. [Graph RAG Basics](#23-graph-rag-basics)
24. [Advanced Graph RAG](#24-advanced-graph-rag)
25. [LLM Integration](#25-llm-integration)
26. [Large-Scale Clustering](#26-large-scale-clustering)
27. [Neo4j Internals](#27-neo4j-internals)
28. [Performance Optimization](#28-performance-optimization)
29. [Enterprise Deployment](#29-enterprise-deployment)
30. [REST API Development](#30-rest-api-development)

---

# 1. Introduction to Graph Databases

## 1.1 What is a Graph Database?

A graph database stores data as **nodes** (entities), **relationships** (connections), and **properties** (attributes). Unlike relational databases that use tables and joins, graph databases represent connections as first-class citizens, enabling efficient traversal of connected data.

### The Property Graph Model

```
┌─────────────────────────────────────────────────────────────────────┐
│                    PROPERTY GRAPH MODEL                             │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│     ┌─────────────────────┐         ┌─────────────────────┐        │
│     │      NODE           │         │      NODE           │        │
│     │  Labels: [:Patient] │         │  Labels: [:Disease] │        │
│     │                     │         │                     │        │
│     │  Properties:        │         │  Properties:        │        │
│     │    id: "P001"       │         │    id: "E11"        │        │
│     │    name: "John"     │         │    name: "Diabetes" │        │
│     │    age: 45          │         │    category: "Endo" │        │
│     └──────────┬──────────┘         └──────────┬──────────┘        │
│                │                               │                    │
│                │    ┌────────────────────┐     │                    │
│                └───►│   RELATIONSHIP     │◄────┘                    │
│                     │  Type: DIAGNOSED   │                          │
│                     │                    │                          │
│                     │  Properties:       │                          │
│                     │    date: 2024-01   │                          │
│                     │    severity: "mod" │                          │
│                     └────────────────────┘                          │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
```

### Graph vs Relational: A Comparison

| Aspect | Relational Database | Graph Database |
|--------|---------------------|----------------|
| **Data Model** | Tables with rows and columns | Nodes, relationships, properties |
| **Joins** | Expensive at runtime | Pre-computed via relationships |
| **Schema** | Fixed, requires migrations | Flexible, schema-optional |
| **Traversal** | Multiple JOINs | Direct navigation |
| **Performance** | Degrades with JOIN depth | Constant for local queries |
| **Use Case** | Structured, tabular data | Connected, relationship-rich data |

### When to Use a Graph Database

Graph databases excel when:

1. **Relationships are important** - Social networks, recommendations, fraud detection
2. **Data is highly connected** - Knowledge graphs, network analysis
3. **Query patterns involve traversal** - Finding paths, patterns, clusters
4. **Schema evolves frequently** - Agile development, prototyping
5. **Real-time queries on connected data** - Recommendations, authorization

## 1.2 Why Neo4j?

Neo4j is the world's leading graph database, offering:

| Feature | Description |
|---------|-------------|
| **Native Graph Storage** | Data stored as graphs, not converted from tables |
| **Cypher Query Language** | Intuitive, pattern-matching syntax |
| **ACID Transactions** | Full transactional guarantees |
| **Graph Data Science** | 70+ graph algorithms built-in |
| **Vector Search** | Native HNSW vector indexes since 5.11 |
| **Scalability** | Handles billions of nodes and relationships |

## 1.3 Core Concepts

### Nodes
Nodes represent entities in your domain. Each node can have:
- **Labels**: Categories or types (e.g., `:Patient`, `:Disease`)
- **Properties**: Key-value pairs (e.g., `name: "John"`)

### Relationships
Relationships connect nodes and always have:
- **Type**: The relationship category (e.g., `DIAGNOSED_WITH`)
- **Direction**: Start node → End node
- **Properties**: Optional key-value pairs

### Properties
Properties store data on both nodes and relationships:
- Supported types: String, Integer, Float, Boolean, Date, DateTime, Point, Lists
- Properties are schema-optional (no predefined columns)

---

# 2. Environment Setup

## 2.1 Docker Installation

The easiest way to run Neo4j is with Docker. Create a `docker-compose.yml` file:

```yaml
version: '3.8'

services:
  neo4j:
    image: neo4j:5.15.0-enterprise
    container_name: neo4j-tutorial
    ports:
      - "7474:7474"  # HTTP Browser
      - "7687:7687"  # Bolt Protocol
    environment:
      - NEO4J_AUTH=neo4j/password123
      - NEO4J_ACCEPT_LICENSE_AGREEMENT=yes
      - NEO4J_PLUGINS=["graph-data-science", "apoc"]
      - NEO4J_dbms_memory_pagecache_size=2G
      - NEO4J_dbms_memory_heap_initial__size=1G
      - NEO4J_dbms_memory_heap_max__size=2G
      - NEO4J_dbms_security_procedures_unrestricted=gds.*,apoc.*
    volumes:
      - neo4j_data:/data
      - neo4j_logs:/logs
      - neo4j_plugins:/plugins
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost:7474"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  neo4j_data:
  neo4j_logs:
  neo4j_plugins:
```

Start Neo4j:

```bash
docker-compose up -d
```

Verify it's running:

```bash
docker-compose logs -f neo4j
# Wait for "Started." message
```

## 2.2 Python Environment

Create a virtual environment and install dependencies:

```bash
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
pip install -r requirements.txt
```

**requirements.txt:**

```text
# Neo4j Driver
neo4j==5.15.0

# Graph Data Science
graphdatascience==1.7.0

# Vector Embeddings
sentence-transformers==2.2.2
openai==1.6.1

# Data Processing
pandas==2.1.4
numpy==1.26.3

# API Server
fastapi==0.109.0
uvicorn==0.25.0

# Utilities
python-dotenv==1.0.0
```

## 2.3 Connection Configuration

Create a `.env` file:

```env
NEO4J_URI=bolt://localhost:7687
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=password123
NEO4J_DATABASE=neo4j
```

## 2.4 Python Connection Module

**src/utils/connection.py:**

```python
"""
Neo4j Connection Utilities
==========================
Centralized connection management for Neo4j.
"""

import os
from contextlib import contextmanager
from typing import Optional, Dict, Any, List
from dotenv import load_dotenv
from neo4j import GraphDatabase, Driver

load_dotenv()


class Neo4jConnection:
    """Neo4j connection manager with pooling support."""
    
    _instance = None
    
    def __init__(self):
        self.uri = os.getenv("NEO4J_URI", "bolt://localhost:7687")
        self.username = os.getenv("NEO4J_USERNAME", "neo4j")
        self.password = os.getenv("NEO4J_PASSWORD", "password123")
        self.database = os.getenv("NEO4J_DATABASE", "neo4j")
        self._driver = None
    
    def connect(self):
        """Establish connection to Neo4j."""
        if self._driver is None:
            self._driver = GraphDatabase.driver(
                self.uri,
                auth=(self.username, self.password),
                max_connection_pool_size=50
            )
        return self
    
    def close(self):
        """Close the connection."""
        if self._driver:
            self._driver.close()
            self._driver = None
    
    @property
    def driver(self) -> Driver:
        """Get driver, connecting if necessary."""
        if self._driver is None:
            self.connect()
        return self._driver
    
    def __enter__(self):
        return self.connect()
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()
    
    @contextmanager
    def session(self, database: str = None):
        """Get a session with context management."""
        session = self.driver.session(database=database or self.database)
        try:
            yield session
        finally:
            session.close()
    
    def execute_query(self, query: str, parameters: Dict = None) -> List[Dict]:
        """Execute a Cypher query and return results."""
        with self.session() as session:
            result = session.run(query, parameters or {})
            return [record.data() for record in result]
    
    def health_check(self) -> Dict[str, Any]:
        """Check database connectivity."""
        try:
            self.driver.verify_connectivity()
            return {"status": "healthy", "uri": self.uri}
        except Exception as e:
            return {"status": "error", "message": str(e)}


# Singleton instance
_connection: Optional[Neo4jConnection] = None


def get_connection() -> Neo4jConnection:
    """Get singleton connection instance."""
    global _connection
    if _connection is None:
        _connection = Neo4jConnection()
        _connection.connect()
    return _connection


def get_driver() -> Driver:
    """Get Neo4j driver from singleton connection."""
    return get_connection().driver
```

## 2.5 Verify Installation

Test your setup:

```python
from src.utils.connection import get_connection

conn = get_connection()
health = conn.health_check()
print(health)  # {'status': 'healthy', 'uri': 'bolt://localhost:7687'}

# Test query
result = conn.execute_query("RETURN 'Hello Neo4j!' as message")
print(result)  # [{'message': 'Hello Neo4j!'}]
```

---

# 3. Nodes, Labels, and Properties

## 3.1 Understanding Nodes

Nodes are the fundamental entities in a graph. Think of them as the "nouns" - people, places, things, concepts.

### Node Anatomy

```
┌─────────────────────────────────────────┐
│              NODE ANATOMY               │
├─────────────────────────────────────────┤
│                                         │
│   Labels: [:Patient, :VIP]              │
│   ─────────────────────────             │
│   Nodes can have multiple labels        │
│   Labels are used for indexing          │
│                                         │
│   Properties:                           │
│   ───────────                           │
│     id: "P001"          (String)        │
│     name: "John Doe"    (String)        │
│     age: 45             (Integer)       │
│     active: true        (Boolean)       │
│     admitted: date()    (Date)          │
│     allergies: [...]    (List)          │
│     location: point()   (Spatial)       │
│                                         │
└─────────────────────────────────────────┘
```

## 3.2 Property Types

Neo4j supports these property types:

| Type | Example | Notes |
|------|---------|-------|
| String | `"John Doe"` | UTF-8 encoded |
| Integer | `45` | 64-bit signed |
| Float | `3.14159` | 64-bit IEEE 754 |
| Boolean | `true` / `false` | |
| Date | `date('2024-01-15')` | ISO 8601 |
| LocalTime | `localtime('12:30:00')` | No timezone |
| DateTime | `datetime()` | With timezone |
| Duration | `duration('P1M2D')` | ISO 8601 duration |
| Point | `point({x:1, y:2})` | 2D/3D spatial |
| List | `['a', 'b', 'c']` | Homogeneous |

## 3.3 Creating Nodes with Cypher

### Basic Node Creation

```cypher
-- Create a simple node
CREATE (p:Patient {id: 'P001', name: 'John Doe'})
RETURN p

-- Create with multiple labels
CREATE (p:Patient:VIP {id: 'P002', name: 'Jane Smith'})
RETURN p

-- Create with various property types
CREATE (p:Patient {
    id: 'P003',
    name: 'Bob Wilson',
    age: 45,
    active: true,
    dateOfBirth: date('1979-03-15'),
    allergies: ['Penicillin', 'Latex'],
    createdAt: datetime()
})
RETURN p
```

### MERGE: Create If Not Exists

```cypher
-- MERGE is idempotent - creates only if not exists
MERGE (p:Patient {id: 'P001'})
ON CREATE SET p.name = 'John Doe', p.createdAt = datetime()
ON MATCH SET p.updatedAt = datetime()
RETURN p
```

## 3.4 Creating Nodes with Python

**src/02_nodes_and_properties.py:**

```python
from dataclasses import dataclass
from datetime import date
from typing import List, Optional
from src.utils.connection import get_driver


@dataclass
class Patient:
    """Patient entity model."""
    id: str
    name: str
    date_of_birth: date
    gender: str
    blood_type: Optional[str] = None
    allergies: List[str] = None
    active: bool = True


class NodeManager:
    """Manages node operations in Neo4j."""
    
    def __init__(self, driver=None):
        self.driver = driver or get_driver()
    
    def create_patient(self, patient: Patient) -> dict:
        """Create a Patient node."""
        query = """
        CREATE (p:Patient {
            id: $id,
            name: $name,
            dateOfBirth: date($dob),
            gender: $gender,
            bloodType: $blood_type,
            allergies: $allergies,
            active: $active,
            createdAt: datetime()
        })
        RETURN p
        """
        
        with self.driver.session() as session:
            result = session.run(
                query,
                id=patient.id,
                name=patient.name,
                dob=patient.date_of_birth.isoformat(),
                gender=patient.gender,
                blood_type=patient.blood_type,
                allergies=patient.allergies or [],
                active=patient.active
            )
            return dict(result.single()["p"])
    
    def bulk_create_patients(self, patients: List[Patient], batch_size: int = 1000) -> int:
        """Bulk create patients using UNWIND for efficiency."""
        query = """
        UNWIND $patients AS p
        CREATE (patient:Patient {
            id: p.id,
            name: p.name,
            dateOfBirth: date(p.dob),
            gender: p.gender,
            createdAt: datetime()
        })
        RETURN count(*) as created
        """
        
        patient_data = [
            {
                "id": p.id,
                "name": p.name,
                "dob": p.date_of_birth.isoformat(),
                "gender": p.gender
            }
            for p in patients
        ]
        
        total = 0
        with self.driver.session() as session:
            for i in range(0, len(patient_data), batch_size):
                batch = patient_data[i:i + batch_size]
                result = session.run(query, patients=batch)
                total += result.single()["created"]
        
        return total
    
    def find_patient(self, patient_id: str) -> Optional[dict]:
        """Find a patient by ID."""
        query = "MATCH (p:Patient {id: $id}) RETURN p"
        
        with self.driver.session() as session:
            result = session.run(query, id=patient_id)
            record = result.single()
            return dict(record["p"]) if record else None
    
    def search_patients(self, name_contains: str = None, limit: int = 100) -> List[dict]:
        """Search patients with filters."""
        conditions = []
        params = {"limit": limit}
        
        if name_contains:
            conditions.append("toLower(p.name) CONTAINS toLower($name)")
            params["name"] = name_contains
        
        where_clause = "WHERE " + " AND ".join(conditions) if conditions else ""
        
        query = f"""
        MATCH (p:Patient)
        {where_clause}
        RETURN p
        ORDER BY p.name
        LIMIT $limit
        """
        
        with self.driver.session() as session:
            result = session.run(query, **params)
            return [dict(r["p"]) for r in result]


# Usage example
if __name__ == "__main__":
    manager = NodeManager()
    
    # Create a patient
    patient = Patient(
        id="P001",
        name="John Doe",
        date_of_birth=date(1980, 5, 15),
        gender="Male",
        blood_type="O+",
        allergies=["Penicillin"]
    )
    
    created = manager.create_patient(patient)
    print(f"Created: {created}")
    
    # Find patient
    found = manager.find_patient("P001")
    print(f"Found: {found}")
```

---

# 4. Relationships

## 4.1 Relationship Fundamentals

Relationships connect nodes and represent the "verbs" - how entities are connected.

### Relationship Anatomy

```
┌─────────────────────────────────────────────────────────────────────┐
│                    RELATIONSHIP ANATOMY                             │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ┌──────────┐                                    ┌──────────┐      │
│  │ Patient  │                                    │ Disease  │      │
│  │          │──[DIAGNOSED_WITH {                 │          │      │
│  │  P001    │    diagnosedDate: date(),          │   E11    │      │
│  │          │    diagnosedBy: "Dr. Smith",       │          │      │
│  │          │    status: "active",               │          │      │
│  │          │    severity: "moderate"            │          │      │
│  └──────────┘  }]──────────────────────────────►└──────────┘      │
│                                                                     │
│     START NODE            RELATIONSHIP             END NODE         │
│                                                                     │
│  Key Points:                                                        │
│  • Relationships are ALWAYS directed                               │
│  • Can be queried in either direction                              │
│  • Properties are optional but useful                              │
│  • Type should be descriptive (verb form)                          │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
```

## 4.2 Relationship Types for Healthcare

| Type | From | To | Properties |
|------|------|-----|------------|
| `DIAGNOSED_WITH` | Patient | Disease | date, status, severity |
| `TAKES` | Patient | Medication | dosage, frequency, start_date |
| `TREATS` | Medication | Disease | effectiveness, first_line |
| `INTERACTS_WITH` | Medication | Medication | severity, description |
| `ALLERGIC_TO` | Patient | Substance | reaction_type, severity |
| `TREATED_BY` | Patient | Doctor | since_date |

## 4.3 Creating Relationships with Cypher

```cypher
-- Create relationship between existing nodes
MATCH (p:Patient {id: 'P001'})
MATCH (d:Disease {id: 'E11'})
CREATE (p)-[r:DIAGNOSED_WITH {
    diagnosedDate: date('2024-01-15'),
    diagnosedBy: 'Dr. Smith',
    status: 'active',
    severity: 'moderate'
}]->(d)
RETURN p, r, d

-- MERGE relationship (create if not exists)
MATCH (p:Patient {id: 'P001'})
MATCH (m:Medication {id: '6809'})
MERGE (p)-[r:TAKES]->(m)
ON CREATE SET r.prescribedDate = date(), r.status = 'active'
ON MATCH SET r.lastRefillDate = date()
RETURN p, r, m
```

## 4.4 Creating Relationships with Python

**src/03_relationships.py:**

```python
from dataclasses import dataclass
from datetime import date
from src.utils.connection import get_driver


@dataclass
class Diagnosis:
    """Diagnosis relationship properties."""
    diagnosed_date: date
    diagnosed_by: str
    status: str = "active"
    severity: str = "moderate"


@dataclass
class Prescription:
    """Prescription relationship properties."""
    prescribed_date: date
    prescribed_by: str
    dosage: str
    frequency: str
    duration_days: int
    refills: int = 0
    status: str = "active"


class RelationshipManager:
    """Manages relationships in Neo4j."""
    
    def __init__(self, driver=None):
        self.driver = driver or get_driver()
    
    def create_diagnosis(self, patient_id: str, disease_id: str, 
                         diagnosis: Diagnosis) -> dict:
        """Create DIAGNOSED_WITH relationship."""
        query = """
        MATCH (p:Patient {id: $patient_id})
        MATCH (d:Disease {id: $disease_id})
        CREATE (p)-[r:DIAGNOSED_WITH {
            diagnosedDate: date($date),
            diagnosedBy: $doctor,
            status: $status,
            severity: $severity,
            createdAt: datetime()
        }]->(d)
        RETURN p.name as patient, d.name as disease, properties(r) as diagnosis
        """
        
        with self.driver.session() as session:
            result = session.run(
                query,
                patient_id=patient_id,
                disease_id=disease_id,
                date=diagnosis.diagnosed_date.isoformat(),
                doctor=diagnosis.diagnosed_by,
                status=diagnosis.status,
                severity=diagnosis.severity
            )
            return result.single().data()
    
    def create_prescription(self, patient_id: str, medication_id: str,
                           prescription: Prescription) -> dict:
        """Create TAKES relationship (prescription)."""
        query = """
        MATCH (p:Patient {id: $patient_id})
        MATCH (m:Medication {id: $med_id})
        CREATE (p)-[r:TAKES {
            dosage: $dosage,
            frequency: $frequency,
            prescribedDate: date($date),
            prescribedBy: $doctor,
            durationDays: $duration,
            refills: $refills,
            status: $status,
            createdAt: datetime()
        }]->(m)
        RETURN p.name as patient, m.name as medication, properties(r) as prescription
        """
        
        with self.driver.session() as session:
            result = session.run(
                query,
                patient_id=patient_id,
                med_id=medication_id,
                dosage=prescription.dosage,
                frequency=prescription.frequency,
                date=prescription.prescribed_date.isoformat(),
                doctor=prescription.prescribed_by,
                duration=prescription.duration_days,
                refills=prescription.refills,
                status=prescription.status
            )
            return result.single().data()
    
    def get_patient_diagnoses(self, patient_id: str) -> list:
        """Get all diagnoses for a patient."""
        query = """
        MATCH (p:Patient {id: $id})-[r:DIAGNOSED_WITH]->(d:Disease)
        RETURN d.name as disease,
               r.diagnosedDate as date,
               r.status as status,
               r.severity as severity
        ORDER BY r.diagnosedDate DESC
        """
        
        with self.driver.session() as session:
            result = session.run(query, id=patient_id)
            return [r.data() for r in result]
    
    def get_patient_medications(self, patient_id: str, active_only: bool = True) -> list:
        """Get medications for a patient."""
        status_filter = "AND r.status = 'active'" if active_only else ""
        
        query = f"""
        MATCH (p:Patient {{id: $id}})-[r:TAKES]->(m:Medication)
        WHERE true {status_filter}
        RETURN m.name as medication,
               m.drugClass as drug_class,
               r.dosage as dosage,
               r.frequency as frequency,
               r.status as status
        """
        
        with self.driver.session() as session:
            result = session.run(query, id=patient_id)
            return [r.data() for r in result]
    
    def check_drug_interactions(self, patient_id: str) -> list:
        """Check for drug interactions in patient's medications."""
        query = """
        MATCH (p:Patient {id: $id})-[t1:TAKES]->(m1:Medication)
        MATCH (p)-[t2:TAKES]->(m2:Medication)
        WHERE t1.status = 'active' AND t2.status = 'active'
          AND m1.id < m2.id
        OPTIONAL MATCH (m1)-[i:INTERACTS_WITH]-(m2)
        WHERE i IS NOT NULL
        RETURN m1.name as drug1,
               m2.name as drug2,
               i.severity as severity,
               i.description as description
        """
        
        with self.driver.session() as session:
            result = session.run(query, id=patient_id)
            return [r.data() for r in result]
```

---

# 5. Basic CRUD Operations

## 5.1 CREATE Operations

```cypher
-- Create single node
CREATE (p:Patient {id: 'P001', name: 'John Doe'})
RETURN p

-- Create multiple nodes
CREATE (p1:Patient {id: 'P002', name: 'Jane Smith'}),
       (p2:Patient {id: 'P003', name: 'Bob Wilson'})
RETURN p1, p2

-- Create with relationship
CREATE (p:Patient {id: 'P004', name: 'Alice'})
CREATE (d:Disease {id: 'E11', name: 'Diabetes'})
CREATE (p)-[:DIAGNOSED_WITH {date: date()}]->(d)
RETURN p, d
```

## 5.2 READ Operations

```cypher
-- Match all patients
MATCH (p:Patient)
RETURN p
LIMIT 100

-- Match by property
MATCH (p:Patient {id: 'P001'})
RETURN p

-- Match with WHERE
MATCH (p:Patient)
WHERE p.age > 40 AND p.active = true
RETURN p.name, p.age
ORDER BY p.age DESC

-- Match patterns
MATCH (p:Patient)-[r:DIAGNOSED_WITH]->(d:Disease)
WHERE p.id = 'P001'
RETURN p.name, d.name, r.diagnosedDate

-- Optional match (left join)
MATCH (p:Patient {id: 'P001'})
OPTIONAL MATCH (p)-[r:TAKES]->(m:Medication)
RETURN p.name, collect(m.name) as medications
```

## 5.3 UPDATE Operations

```cypher
-- Update properties with SET
MATCH (p:Patient {id: 'P001'})
SET p.age = 46, p.updatedAt = datetime()
RETURN p

-- Add property
MATCH (p:Patient {id: 'P001'})
SET p.email = 'john@example.com'
RETURN p

-- Remove property
MATCH (p:Patient {id: 'P001'})
REMOVE p.email
RETURN p

-- Update relationship
MATCH (p:Patient {id: 'P001'})-[r:DIAGNOSED_WITH]->(d:Disease)
SET r.status = 'resolved'
RETURN r
```

## 5.4 DELETE Operations

```cypher
-- Delete node (no relationships)
MATCH (p:Patient {id: 'P001'})
DELETE p

-- Delete node and relationships
MATCH (p:Patient {id: 'P001'})
DETACH DELETE p

-- Delete specific relationship
MATCH (p:Patient {id: 'P001'})-[r:TAKES]->(m:Medication {id: '6809'})
DELETE r

-- Delete all of a label
MATCH (n:TestNode)
DETACH DELETE n
```

## 5.5 Python CRUD Class

**src/04_basic_queries.py:**

```python
class CypherCRUD:
    """Basic CRUD operations using Cypher."""
    
    def __init__(self, driver=None):
        self.driver = driver or get_driver()
    
    # CREATE
    def create_node(self, label: str, properties: dict) -> dict:
        props = ", ".join([f"{k}: ${k}" for k in properties.keys()])
        query = f"""
        CREATE (n:{label} {{{props}}})
        SET n.createdAt = datetime()
        RETURN n
        """
        with self.driver.session() as session:
            result = session.run(query, **properties)
            return dict(result.single()["n"])
    
    # READ
    def find_by_id(self, label: str, id_value: str) -> dict:
        query = f"MATCH (n:{label} {{id: $id}}) RETURN n"
        with self.driver.session() as session:
            result = session.run(query, id=id_value)
            record = result.single()
            return dict(record["n"]) if record else None
    
    def find_all(self, label: str, limit: int = 100) -> list:
        query = f"MATCH (n:{label}) RETURN n LIMIT $limit"
        with self.driver.session() as session:
            result = session.run(query, limit=limit)
            return [dict(r["n"]) for r in result]
    
    # UPDATE
    def update(self, label: str, id_value: str, properties: dict) -> dict:
        set_clause = ", ".join([f"n.{k} = ${k}" for k in properties])
        query = f"""
        MATCH (n:{label} {{id: $id}})
        SET {set_clause}, n.updatedAt = datetime()
        RETURN n
        """
        with self.driver.session() as session:
            result = session.run(query, id=id_value, **properties)
            record = result.single()
            return dict(record["n"]) if record else None
    
    # DELETE
    def delete(self, label: str, id_value: str, detach: bool = True) -> bool:
        cmd = "DETACH DELETE" if detach else "DELETE"
        query = f"""
        MATCH (n:{label} {{id: $id}})
        {cmd} n
        RETURN count(*) as deleted
        """
        with self.driver.session() as session:
            result = session.run(query, id=id_value)
            return result.single()["deleted"] > 0
```

---

# 6. Advanced Cypher Queries

## 6.1 Variable-Length Paths

```cypher
-- Find nodes within 1-3 hops
MATCH path = (p:Patient {id: 'P001'})-[*1..3]-(connected)
RETURN path

-- Specific relationship types
MATCH path = (p:Patient)-[:DIAGNOSED_WITH|TAKES*1..2]-(end)
WHERE p.id = 'P001'
RETURN path

-- Collect path information
MATCH path = (p:Patient {id: 'P001'})-[*1..3]-(connected)
RETURN connected.name,
       length(path) as distance,
       [node in nodes(path) | labels(node)[0]] as node_types
```

## 6.2 Shortest Path

```cypher
-- Find shortest path
MATCH path = shortestPath(
    (p1:Patient {id: 'P001'})-[*]-(p2:Patient {id: 'P002'})
)
RETURN path, length(path)

-- All shortest paths
MATCH paths = allShortestPaths(
    (p1:Patient {id: 'P001'})-[*]-(p2:Patient {id: 'P002'})
)
RETURN paths

-- Shortest with specific relationships
MATCH path = shortestPath(
    (p:Patient {id: 'P001'})-[:DIAGNOSED_WITH|TREATS*]-(m:Medication)
)
RETURN path
```

## 6.3 Aggregations

```cypher
-- Count and collect
MATCH (p:Patient)-[:DIAGNOSED_WITH]->(d:Disease)
RETURN d.name as disease,
       count(p) as patient_count,
       collect(p.name)[0..5] as sample_patients
ORDER BY patient_count DESC

-- Statistics
MATCH (p:Patient)-[r:TAKES]->(m:Medication)
WHERE r.status = 'active'
WITH m, count(p) as prescriptions, avg(p.age) as avg_age
RETURN m.name, prescriptions, round(avg_age, 1)
ORDER BY prescriptions DESC
```

## 6.4 Subqueries

```cypher
-- CALL subquery for per-row calculations
MATCH (p:Patient)
CALL {
    WITH p
    MATCH (p)-[:DIAGNOSED_WITH]->(d)
    RETURN count(d) as disease_count
}
RETURN p.name, disease_count
ORDER BY disease_count DESC

-- EXISTS subquery for filtering
MATCH (p:Patient)
WHERE EXISTS {
    MATCH (p)-[:DIAGNOSED_WITH]->(d:Disease {name: 'Type 2 Diabetes'})
}
RETURN p.name
```

## 6.5 Pattern Comprehension

```cypher
-- Get related entities inline
MATCH (p:Patient {id: 'P001'})
RETURN p.name,
       [(p)-[:DIAGNOSED_WITH]->(d) | d.name] as diseases,
       [(p)-[:TAKES]->(m) | {name: m.name, class: m.drugClass}] as meds
```

---

# 7. Indexing and Constraints

## 7.1 Index Types

```
┌─────────────────────────────────────────────────────────────────┐
│                     INDEX TYPES IN NEO4J                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  RANGE INDEX          For =, <>, <, >, <=, >=, IN              │
│  ───────────          O(log n) lookup using B-tree             │
│                                                                 │
│  TEXT INDEX           For CONTAINS, ENDS WITH                   │
│  ──────────           Optimized for string operations           │
│                                                                 │
│  FULLTEXT INDEX       Natural language search with scoring      │
│  ──────────────       Lucene-based, supports fuzzy matching     │
│                                                                 │
│  VECTOR INDEX         Similarity search on embeddings           │
│  ────────────         HNSW algorithm, O(log n) search          │
│                                                                 │
│  POINT INDEX          Spatial queries                           │
│  ───────────          Distance and bounding box operations      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
```

## 7.2 Creating Indexes

```cypher
-- Range index (most common)
CREATE INDEX patient_id FOR (p:Patient) ON (p.id)

-- Composite index
CREATE INDEX patient_name_gender FOR (p:Patient) ON (p.name, p.gender)

-- Text index
CREATE TEXT INDEX patient_name_text FOR (p:Patient) ON (p.name)

-- Fulltext index
CREATE FULLTEXT INDEX patient_search FOR (n:Patient|Disease)
ON EACH [n.name, n.description]

-- Vector index (HNSW)
CREATE VECTOR INDEX chunk_embeddings FOR (c:Chunk) ON (c.embedding)
OPTIONS {indexConfig: {
    `vector.dimensions`: 384,
    `vector.similarity_function`: 'cosine'
}}
```

## 7.3 Constraints

```cypher
-- Unique constraint (also creates index)
CREATE CONSTRAINT patient_id_unique FOR (p:Patient)
REQUIRE p.id IS UNIQUE

-- Existence constraint (Enterprise)
CREATE CONSTRAINT patient_name_exists FOR (p:Patient)
REQUIRE p.name IS NOT NULL

-- Node key (composite unique + existence)
CREATE CONSTRAINT patient_key FOR (p:Patient)
REQUIRE (p.id, p.source) IS NODE KEY
```

---

# 11. Graph Data Science Basics

## 11.1 GDS Architecture

The Neo4j Graph Data Science library provides 70+ algorithms for:
- Centrality (finding important nodes)
- Community detection (clustering)
- Pathfinding (shortest paths, routing)
- Similarity (finding similar nodes)
- Embeddings (vector representations)
- Link prediction (predicting connections)

## 11.2 Graph Projections

Before running algorithms, you must project a subgraph into memory:

```python
from graphdatascience import GraphDataScience

gds = GraphDataScience("bolt://localhost:7687", auth=("neo4j", "password123"))

# Native projection (faster)
G, result = gds.graph.project(
    "healthcare",
    ["Patient", "Disease", "Medication"],
    ["DIAGNOSED_WITH", "TAKES", "TREATS"]
)

print(f"Nodes: {result['nodeCount']}")
print(f"Relationships: {result['relationshipCount']}")

# Run algorithm
pagerank_result = gds.pageRank.stream(G)
print(pagerank_result.sort_values('score', ascending=False).head(10))

# Cleanup
gds.graph.drop("healthcare")
```

---

# 12. Centrality Algorithms

## 12.1 PageRank

Measures node importance based on incoming connections from other important nodes.

```python
# Stream mode - returns results without writing
result = gds.pageRank.stream(G, dampingFactor=0.85, maxIterations=20)

# Write mode - persists results to database
gds.pageRank.write(G, writeProperty='pagerank')

# Query results
query = """
MATCH (p:Patient)
RETURN p.name, p.pagerank
ORDER BY p.pagerank DESC
LIMIT 10
"""
```

## 12.2 Betweenness Centrality

Measures how often a node lies on shortest paths between other nodes.

```python
result = gds.betweenness.stream(G)
# High betweenness = bridge/broker node
```

## 12.3 Degree Centrality

Simple count of connections.

```python
result = gds.degree.stream(G, orientation='UNDIRECTED')
```

---

# 13. Community Detection

## 13.1 Louvain Algorithm

Detects communities by optimizing modularity.

```python
result = gds.louvain.stream(G)

# Get community statistics
community_sizes = result.groupby('communityId').size()
print(f"Communities found: {len(community_sizes)}")
print(f"Largest community: {community_sizes.max()} nodes")
```

## 13.2 Label Propagation

Fast community detection using neighbor labels.

```python
result = gds.labelPropagation.stream(G, maxIterations=10)
```

---

# 20. Document Chunking

## 20.1 Chunking Strategies

```
┌─────────────────────────────────────────────────────────────────┐
│                    CHUNKING STRATEGIES                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  FIXED-SIZE         Split by character/word count              │
│  ──────────         Simple but may cut mid-sentence            │
│                     Overlap prevents boundary loss              │
│                                                                 │
│  SENTENCE           Split at sentence boundaries                │
│  ────────           Preserves semantic units                    │
│                     Variable chunk sizes                        │
│                                                                 │
│  PARAGRAPH          Split at paragraph breaks                   │
│  ─────────          Respects document structure                │
│                     Good for well-formatted docs               │
│                                                                 │
│  RECURSIVE          Hierarchical splitting                      │
│  ─────────          Try paragraph → sentence → word            │
│                     Adapts to content structure                │
│                                                                 │
│  SEMANTIC           Split when embedding similarity drops       │
│  ────────           Groups related content                     │
│                     Requires embedding model                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
```

## 20.2 Python Implementation

```python
import re
from dataclasses import dataclass
from typing import List


@dataclass
class Chunk:
    id: str
    content: str
    index: int
    start_char: int
    end_char: int


class TextChunker:
    def __init__(self, chunk_size=500, overlap=50):
        self.chunk_size = chunk_size
        self.overlap = overlap
    
    def fixed_size_chunk(self, text: str) -> List[Chunk]:
        """Split into fixed-size chunks with overlap."""
        chunks = []
        start = 0
        index = 0
        
        while start < len(text):
            end = start + self.chunk_size
            
            # Find word boundary
            if end < len(text):
                space_pos = text.rfind(' ', start, end)
                if space_pos > start:
                    end = space_pos + 1
            
            chunk_text = text[start:end].strip()
            if chunk_text:
                chunks.append(Chunk(
                    id=f"chunk_{index}",
                    content=chunk_text,
                    index=index,
                    start_char=start,
                    end_char=end
                ))
                index += 1
            
            start = end - self.overlap
        
        return chunks
    
    def sentence_chunk(self, text: str, sentences_per_chunk: int = 3) -> List[Chunk]:
        """Split by sentences."""
        sentences = re.split(r'(?<=[.!?])\s+', text)
        chunks = []
        
        for i in range(0, len(sentences), sentences_per_chunk):
            chunk_text = ' '.join(sentences[i:i+sentences_per_chunk])
            if chunk_text.strip():
                chunks.append(Chunk(
                    id=f"chunk_{len(chunks)}",
                    content=chunk_text.strip(),
                    index=len(chunks),
                    start_char=0,
                    end_char=len(chunk_text)
                ))
        
        return chunks
```

---

# 21. Vector Embeddings

## 21.1 Embedding Generation

```python
from sentence_transformers import SentenceTransformer


class EmbeddingGenerator:
    def __init__(self, model_name="all-MiniLM-L6-v2"):
        self.model = SentenceTransformer(model_name)
        self.dimensions = 384  # Model-specific
    
    def embed(self, text: str) -> list:
        """Generate embedding for text."""
        return self.model.encode(text).tolist()
    
    def embed_batch(self, texts: list) -> list:
        """Generate embeddings for batch."""
        embeddings = self.model.encode(texts)
        return [e.tolist() for e in embeddings]
```

## 21.2 Storing in Neo4j

```python
def store_chunk_embeddings(driver, chunks, embeddings):
    query = """
    UNWIND $data AS item
    MERGE (c:Chunk {id: item.id})
    SET c.content = item.content,
        c.embedding = item.embedding
    """
    
    data = [
        {"id": c.id, "content": c.content, "embedding": e}
        for c, e in zip(chunks, embeddings)
    ]
    
    with driver.session() as session:
        session.run(query, data=data)
```

---

# 22. HNSW Indexing

## 22.1 HNSW Algorithm

```
┌─────────────────────────────────────────────────────────────────┐
│                    HNSW STRUCTURE                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Layer 2:  [A]━━━━━━━━━━━━━━━━━━━━━━[B]     (sparse, long)    │
│             │                        │                          │
│  Layer 1:  [A]━━━━[C]━━━━━━━━[D]━━━[B]     (medium)            │
│             │     │          │      │                          │
│  Layer 0:  [A]─[E]─[C]─[F]─[D]─[G]─[B]     (dense, all nodes) │
│                                                                 │
│  Search: Enter at top, greedy navigate down                    │
│  Complexity: O(log n) approximate nearest neighbor             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
```

## 22.2 Creating Vector Index

```cypher
CREATE VECTOR INDEX chunk_vectors FOR (c:Chunk) ON (c.embedding)
OPTIONS {indexConfig: {
    `vector.dimensions`: 384,
    `vector.similarity_function`: 'cosine'
}}
```

## 22.3 Similarity Search

```python
def similarity_search(driver, query_embedding, top_k=10):
    query = """
    CALL db.index.vector.queryNodes('chunk_vectors', $k, $embedding)
    YIELD node, score
    RETURN node.id as id, node.content as content, score
    ORDER BY score DESC
    """
    
    with driver.session() as session:
        result = session.run(query, k=top_k, embedding=query_embedding)
        return [r.data() for r in result]
```

---

# 23. Graph RAG Basics

## 23.1 Graph RAG Architecture

```
┌─────────────────────────────────────────────────────────────────┐
│                    GRAPH RAG PIPELINE                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  USER QUERY: "What medications treat diabetes?"                │
│       │                                                         │
│       ▼                                                         │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ 1. EMBED QUERY                                          │   │
│  │    Generate vector from question                        │   │
│  └─────────────────────────────────────────────────────────┘   │
│       │                                                         │
│       ▼                                                         │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ 2. VECTOR SEARCH                                        │   │
│  │    Find similar chunks using HNSW                       │   │
│  └─────────────────────────────────────────────────────────┘   │
│       │                                                         │
│       ▼                                                         │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ 3. GRAPH TRAVERSAL                                      │   │
│  │    Expand to related entities via relationships         │   │
│  └─────────────────────────────────────────────────────────┘   │
│       │                                                         │
│       ▼                                                         │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ 4. CONTEXT ASSEMBLY                                     │   │
│  │    Combine chunks + entities + relationships            │   │
│  └─────────────────────────────────────────────────────────┘   │
│       │                                                         │
│       ▼                                                         │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ 5. LLM GENERATION                                       │   │
│  │    Generate grounded answer                             │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
```

## 23.2 Implementation

```python
class GraphRAG:
    def __init__(self, driver, embeddings):
        self.driver = driver
        self.embeddings = embeddings
    
    def query(self, question: str, top_k: int = 5):
        # 1. Embed query
        query_embedding = self.embeddings.embed(question)
        
        # 2. Vector search
        chunks = self._vector_search(query_embedding, top_k)
        
        # 3. Graph traversal
        entities = self._get_related_entities([c['id'] for c in chunks])
        graph_context = self._get_graph_context(entities)
        
        # 4. Build context
        context = self._build_context(chunks, entities, graph_context)
        
        return {"context": context, "chunks": chunks, "entities": entities}
    
    def _vector_search(self, embedding, top_k):
        query = """
        CALL db.index.vector.queryNodes('chunk_vectors', $k, $embedding)
        YIELD node, score
        RETURN node.id as id, node.content as content, score
        """
        with self.driver.session() as session:
            result = session.run(query, k=top_k, embedding=embedding)
            return [r.data() for r in result]
    
    def _get_related_entities(self, chunk_ids):
        query = """
        UNWIND $ids AS cid
        MATCH (c:Chunk {id: cid})-[:MENTIONS]->(e)
        RETURN DISTINCT labels(e)[0] as label, e.name as name
        """
        with self.driver.session() as session:
            result = session.run(query, ids=chunk_ids)
            return [r.data() for r in result]
    
    def _get_graph_context(self, entities):
        # Get relationships between entities
        lines = []
        for e in entities:
            lines.append(f"- {e['label']}: {e['name']}")
        return "\n".join(lines)
    
    def _build_context(self, chunks, entities, graph_context):
        sections = ["=== RELEVANT PASSAGES ==="]
        for i, c in enumerate(chunks, 1):
            sections.append(f"[{i}] {c['content']}")
        
        if entities:
            sections.append("\n=== RELATED ENTITIES ===")
            sections.append(graph_context)
        
        return "\n".join(sections)
```

---

# 26. Large-Scale Clustering

## 26.1 Handling 10K+ Nodes

When graphs exceed 10,000 nodes around a single entity, you need hierarchical strategies:

```
┌─────────────────────────────────────────────────────────────────┐
│            HIERARCHICAL CLUSTERING                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Level 3: Region Summaries                                      │
│  ┌─────────┐    ┌─────────┐    ┌─────────┐                     │
│  │Northeast│    │ Midwest │    │  West   │                     │
│  │  5000   │    │  3000   │    │  2000   │                     │
│  └────┬────┘    └────┬────┘    └────┬────┘                     │
│       │              │              │                           │
│  Level 2: Cluster Summaries                                     │
│  ┌───┴───┐      ┌───┴───┐      ┌───┴───┐                       │
│  │Cluster│      │Cluster│      │Cluster│                       │
│  │  500  │ ...  │  300  │ ...  │  200  │                       │
│  └───┬───┘      └───┬───┘      └───┬───┘                       │
│      │              │              │                            │
│  Level 1: Individual Patients                                   │
│  ┌─┬─┬─┬─┐      ┌─┬─┬─┬─┐      ┌─┬─┬─┬─┐                       │
│  │P│P│P│P│ ...  │P│P│P│P│ ...  │P│P│P│P│                       │
│  └─┴─┴─┴─┘      └─┴─┴─┴─┘      └─┴─┴─┴─┘                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
```

## 26.2 Implementation

```python
class LargeScaleManager:
    def __init__(self, driver):
        self.driver = driver
    
    def cluster_patients(self, write_property="cluster"):
        """Use Louvain to cluster patients."""
        # Create similarity projection
        create_query = """
        CALL gds.graph.project.cypher(
            'patient_similarity',
            'MATCH (p:Patient) RETURN id(p) AS id',
            'MATCH (p1:Patient)-[:DIAGNOSED_WITH]->(d)<-[:DIAGNOSED_WITH]-(p2:Patient)
             WHERE id(p1) < id(p2)
             RETURN id(p1) AS source, id(p2) AS target, count(d) AS weight'
        )
        """
        
        louvain_query = """
        CALL gds.louvain.write('patient_similarity', {
            writeProperty: $prop
        })
        YIELD communityCount, modularity
        RETURN communityCount, modularity
        """
        
        with self.driver.session() as session:
            session.run(create_query)
            result = session.run(louvain_query, prop=write_property)
            return result.single().data()
    
    def create_cluster_summaries(self):
        """Create summary nodes for clusters."""
        query = """
        MATCH (p:Patient)
        WHERE p.cluster IS NOT NULL
        WITH p.cluster AS clusterId, collect(p) AS patients
        MERGE (cs:ClusterSummary {clusterId: clusterId})
        SET cs.patientCount = size(patients)
        WITH cs, patients
        UNWIND patients AS p
        MERGE (p)-[:BELONGS_TO]->(cs)
        """
        with self.driver.session() as session:
            session.run(query)
```

---

# 27. Neo4j Internals

## 27.1 Storage Architecture

```
┌─────────────────────────────────────────────────────────────────┐
│                    NEO4J STORAGE                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  neostore.nodestore.db                                          │
│  ├── Fixed-size records (15 bytes)                             │
│  ├── Direct offset access: O(1)                                │
│  └── Contains: labels, first_rel, first_prop                   │
│                                                                 │
│  neostore.relationshipstore.db                                  │
│  ├── Fixed-size records (34 bytes)                             │
│  ├── Doubly-linked lists for traversal                         │
│  └── Contains: type, start, end, next_rel, prev_rel            │
│                                                                 │
│  neostore.propertystore.db                                      │
│  ├── Variable-length property chains                           │
│  └── Contains: key, value, next_prop                           │
│                                                                 │
│  PAGE CACHE (in memory)                                         │
│  ├── Caches frequently accessed pages                          │
│  ├── Configurable size                                         │
│  └── LRU eviction                                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
```

## 27.2 Why Traversal is Fast

- **Index-free adjacency**: Relationships are pointers, not lookups
- **Constant-time local queries**: Performance independent of total graph size
- **Relationship chains**: Doubly-linked for efficient bidirectional traversal

---

# 30. REST API Development

## 30.1 FastAPI Server

```python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional

app = FastAPI(title="Neo4j Knowledge Graph API")


class NodeCreate(BaseModel):
    labels: List[str]
    properties: dict


class QueryRequest(BaseModel):
    query: str
    parameters: Optional[dict] = None


@app.post("/nodes")
async def create_node(node: NodeCreate):
    labels = ":".join(node.labels)
    props = ", ".join([f"{k}: ${k}" for k in node.properties])
    query = f"CREATE (n:{labels} {{{props}}}) RETURN n"
    
    with driver.session() as session:
        result = session.run(query, **node.properties)
        return {"node": dict(result.single()["n"])}


@app.get("/nodes/{label}/{id}")
async def get_node(label: str, id: str):
    query = f"MATCH (n:{label} {{id: $id}}) RETURN n"
    with driver.session() as session:
        result = session.run(query, id=id)
        record = result.single()
        if not record:
            raise HTTPException(404, "Node not found")
        return {"node": dict(record["n"])}


@app.post("/query")
async def execute_query(request: QueryRequest):
    with driver.session() as session:
        result = session.run(request.query, request.parameters or {})
        return {"results": [r.data() for r in result]}


@app.post("/rag/query")
async def rag_query(question: str, top_k: int = 5):
    rag = GraphRAG(driver, embeddings)
    return rag.query(question, top_k)


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)
```

---

# Quick Reference

## Essential Cypher Commands

```cypher
-- Create
CREATE (n:Label {prop: value})
MERGE (n:Label {id: $id}) ON CREATE SET n.created = datetime()

-- Read
MATCH (n:Label {id: $id}) RETURN n
MATCH (a)-[r:REL]->(b) RETURN a, r, b

-- Update
MATCH (n {id: $id}) SET n.prop = value
MATCH (n {id: $id}) REMOVE n.prop

-- Delete
MATCH (n {id: $id}) DELETE n
MATCH (n {id: $id}) DETACH DELETE n

-- Index
CREATE INDEX name FOR (n:Label) ON (n.property)
CREATE VECTOR INDEX name FOR (n:Label) ON (n.embedding) OPTIONS {...}

-- GDS
CALL gds.graph.project('name', ['Label'], ['REL'])
CALL gds.pageRank.stream('name') YIELD nodeId, score
CALL db.index.vector.queryNodes('index', k, embedding) YIELD node, score
```

---

# Performance Targets

| Operation | Target | How to Achieve |
|-----------|--------|----------------|
| Indexed lookup | <1ms | Range index on property |
| 2-hop traversal | <5ms | Native graph storage |
| Vector search (10K) | <10ms | HNSW index |
| PageRank (10K) | <500ms | GDS streaming |
| Bulk insert (1K) | <100ms | UNWIND pattern |

---

*End of Tutorial*

**Version:** 1.0  
**Last Updated:** January 2025
